package hhf.flex.soundapi.core {
	import flash.events.Event;
	import flash.events.SampleDataEvent;
	import flash.filesystem.File;
	import flash.filesystem.FileMode;
	import flash.filesystem.FileStream;
	import flash.media.ID3Info;
	import flash.media.Sound;
	import flash.media.SoundChannel;
	import flash.media.SoundLoaderContext;
	import flash.media.SoundTransform;
	import flash.net.URLRequest;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	
	import hhf.flex.soundapi.core.chunks.FmtChunk;
	import hhf.flex.soundapi.core.chunks.InfoChunk;
	import hhf.flex.soundapi.core.chunks.ListChunk;
	

	/**
	 * Sound au foramt wav, à utiliser comme un sound normal
	 */
	public class WavSound extends Sound	{
		// debug mode
		private var debug:Boolean = false;

		// Frequence d'échantillonage
		public var sampleRate:int = 44100;

		// type d'interpolation
		private var _interpolationType:InterpolationType = InterpolationType.SAMPLE;
		public function get interpolationType():InterpolationType {
			return _interpolationType;
		}
		public function set interpolationType(value:InterpolationType):void {
			trace("InterpolationType : "+value);
			_interpolationType = value;
		}

		private var _bytesTotal:int = 0;
		private var _url:String = null;
		private var _id3:ID3Info = null;
		private var _len:Number = 0;
		// CHUNKS
		public var listChunk:ListChunk;
		public var fmtChunk:FmtChunk;
		// le stream du wav, dans sont integralité, les datas sont a startOffsetDatas
		private var inStream:FileStream = new FileStream();
		private var startOffsetDatas:int = 0;

		// readShort:16bit methode de lecture du son en fonction de nombre de bits par echantillon
		private var readMethodName:String = "readShort";
		// nombre d'octets pour chaque sample, dans le fichier source. Permet d'eviter de tenter de lire au dela du fichier 
		// readByte donc 8bit = 1 octet. 
		// readShort donc 16bit = 2 octet. 
		// readInt donc 32bit = 4 octet. 
		private var nbBytesBySample:uint = 2; 
		// pour transformer le brut en float
		private var factorToFloat:int = 0; // si sample sur 16bit alors 2^16 = 32768

		// simule un semaphore, on verrouile l'extraction de data (methode extract)  pendant la generation du son
		private var locked:Boolean = false;
		private var playposition:uint = 0;

		private var t0:Number = 0;
		private var wast:Number = 0;

		/**
		 * Constructeur
		 */
		public function WavSound(stream:URLRequest=null, context:SoundLoaderContext=null) {
			super();
			this.addEventListener(SampleDataEvent.SAMPLE_DATA, playSoundHandler);
			this.load(stream, context);
		}

		/**
		 * Calcul polynomial de l'interpolation
		 */
		private function getYPolynomial(ys:Array, x:Number, factor:Number):Number {
			var y:Number = 0;
			for(var i:Number = 0; i<4; i++) {
				var numerateur:Number = 1; 
				var denominateur:Number = 1;
				var produit:int = ys[i];
				for(var j:Number = 0; j<4; j++) {
					if(i!=j){
						numerateur = numerateur * (x-(j*factor));
						denominateur = denominateur * (i-j) * factor;
					}
				}
				y += ((produit*numerateur)/denominateur);
			}
			return y;
		} 
		
		/**
		 * Calcul lineaire de l'interpolation
		 */
		private function getYLinear(ys:Array, x:Number, factor:Number):Number {
			var p:Number = ys[1]-ys[0];
			return p * x + ys[0];  
		} 

		/**
		 * Quand le sound est en play on inject les données du wav
		 * avec interpolation si nécessaire
		 * @private
		 */
		private function playSoundHandler(event:SampleDataEvent):void {
			var returnBytes:ByteArray = event.data;
			var buffer:ByteArray = new ByteArray();
			buffer.endian = Endian.LITTLE_ENDIAN;
			locked = true; // verrouillage de l'extraction
			inStream.position = playposition;
			if(inStream.bytesAvailable) {// 						 8192/(8/4) * 4
				var nbChannel:int = fmtChunk.numChannels;
				var factor:Number = 44100/sampleRate;
				var ifactor:int = int(factor);
				wast += (factor-ifactor);
				ifactor += Math.floor(wast);
//				wast -= Math.floor(wast);
				var nbSamplesLRRead:int = (8192*nbChannel)/ifactor;
				var nbBytes:int = Math.min(inStream.bytesAvailable, nbSamplesLRRead*nbBytesBySample);
				inStream.readBytes(buffer, 0, nbBytes);
				playposition = inStream.position;
				locked = false;
				if(ifactor==1) { // echantillonage 44100
					while(buffer.bytesAvailable>=(nbBytesBySample*nbChannel)) { 
						var l0:int = buffer[readMethodName](); // lit 2
						var r0:int = 0;
						if(fmtChunk.numChannels==2) { // si c'est stereo
							r0 = buffer[readMethodName](); // lit 2
						}
						returnBytes.writeFloat(transformToFloat(l0)); // ecrit 4
						returnBytes.writeFloat(transformToFloat(r0)); // ecrit 4
					}
				} else { // l'echantillonage n'est pas en 44100
					var lx0:int = buffer[readMethodName](); // lit 2
					var rx0:int = buffer[readMethodName](); // lit 2
					var lxs:Array = [lx0];
					var rxs:Array = [rx0];
					returnBytes.writeFloat(transformToFloat(lx0));
					returnBytes.writeFloat(transformToFloat(rx0));
					var bcl:int = interpolationType.nbsample*ifactor;
					while(buffer.bytesAvailable>=(nbBytesBySample*nbChannel*interpolationType.nbsample)) {
						for(var i:int=0; i<interpolationType.nbsample-1; i++) {
							lxs.push(buffer[readMethodName]());
							rxs.push(buffer[readMethodName]());
						}
						var lx1:int = buffer[readMethodName]();
						var rx1:int = buffer[readMethodName]();
						lxs.push(lx1);
						rxs.push(rx1);
						for(var b:int = 0; b<bcl; b++) {
							if(InterpolationType.SAMPLE.equals(interpolationType)) {
								returnBytes.writeFloat(transformToFloat(lx1));
								returnBytes.writeFloat(transformToFloat(rx1));
							} else if(InterpolationType.LINEAR.equals(interpolationType)) {
								returnBytes.writeFloat(transformToFloat(getYLinear(lxs, b, ifactor)));
								returnBytes.writeFloat(transformToFloat(getYLinear(rxs, b, ifactor)));
							} else if(InterpolationType.POLYNOMIAL.equals(interpolationType)) {
								returnBytes.writeFloat(transformToFloat(getYPolynomial(lxs, b, ifactor)));
								returnBytes.writeFloat(transformToFloat(getYPolynomial(rxs, b, ifactor)));
							}
						}
						lxs = [lx1];
						rxs = [rx1];
					}
				}
			} else {
				locked = false;
			}
		}
		
		/**
		 * Methode d'extraction du sound
		 * @param target : le buffer recevant les données
		 * @param length : le nombre de sample left/right extrait
		 * @param startPosition : combien de sample left/right on ignore.
		 * Le format des samples sont compris entre -1 et +1. 
		 * @return : -1 qd c'est finit, sinon pas finit
		 * Peut retourner 0 sans que cela soit finit, en effet, le stream utiliser dans l'extraction est le même que pour la lecture
		 * Aussi, un lock est posé sur la zone critique de lecture et empeche donc la lecture de donnée lors de l'extraction
		 */
		override public function extract(target:ByteArray, length:Number, startPosition:Number=0):Number {
			// startPosition = nombre de couple de sample deja lu
			if(locked) return 0;
			inStream.position = startOffsetDatas + (startPosition * (fmtChunk.bitsPerSample/8) * fmtChunk.numChannels); // un sample en octet = fmtChunk.bitsPerSample/8  
			var numSampleRead:Number = 0;
			while(numSampleRead<length) { 
				if(inStream.bytesAvailable>=nbBytesBySample*fmtChunk.numChannels) {
					target.writeFloat(transformToFloat(inStream[readMethodName]()));
					if(fmtChunk.numChannels==2) {
						target.writeFloat(transformToFloat(inStream[readMethodName]()));
					} else {
						target.writeFloat(0);
					}
				} else break; // cela ne devrait pas arrivé
				numSampleRead++;
			}
			if(!numSampleRead) return -1;
			return numSampleRead;
		}

		/**
		 * Methode d'extraction du sound au format PCM, plus rapide, mais plus complexe à post traiter
		 * @param target : le buffer recevant les données
		 * @param length : le nombre de sample left/right extrait
		 * @param startPosition : combien de sample left/right on ignore.
		 * Le format des samples dépend de l'encodage. 
		 * 31bit entre -2147483648 et 2147483648 => -/+ 2^31 => readInt 
		 * 16bit entre -32768 et 32768 => -/+ 2^15 => readShort 
		 * 8bit entre -128 et 128 => -/+ 2^7 => readByte
		 * Il faut aussi tenir compte du nombre de canaux
		 * @return : -1 qd c'est finit, sinon pas finit
		 * Peut retourner 0 sans que cela soit finit, en effet, le stream utiliser dans l'extraction est le même que pour la lecture
		 * Aussi, un lock est posé sur la zone critique de lecture et empeche donc la lecture de donnée lors de l'extraction
		 */
		public function extractPcm(target:ByteArray, length:Number, startPosition:Number=0):Number {
			target.endian = Endian.LITTLE_ENDIAN;
			// startPosition = nombre de couple de sample deja lu
			if(locked) return 0;
			inStream.position = startOffsetDatas + (startPosition * (fmtChunk.bitsPerSample/8) * fmtChunk.numChannels); // un sample en octet = fmtChunk.bitsPerSample/8  
			var numSampleRead:Number = 0;
			var len:int = Math.min(length * (fmtChunk.bitsPerSample/8) * fmtChunk.numChannels, inStream.bytesAvailable);
			inStream.readBytes(target, 0, len);
			numSampleRead = target.length / ((fmtChunk.bitsPerSample/8) * fmtChunk.numChannels);
			if(!numSampleRead) return -1;
			return numSampleRead;
		}

		/**
		 * Analyse du wav, decoupage des chunks
		 * @private
		 */
		private function analyseWav(stream:URLRequest):void {
			var inFile:File = new File(stream.url);
			inStream.open(inFile, FileMode.READ);
			inStream.endian = Endian.LITTLE_ENDIAN;
			var offset:int = 0;
			// LECTURE DU FORMAT
			var format:String = inStream.readMultiByte(4, "utf-8"); // 4 octets
			log(offset+"-3 : Format : "+format);
			if(format!="RIFF") {
				throw new Error("UnsuportedFormat : "+format);
			}
			// LECTURE DE LA TAILLE DU FICHIER
			offset +=4;
			var filelen:int = inStream.readInt(); // 4 octets
			log(offset+"-7 : Taille totale du fichier en octet: "+filelen); 
			// LECTURE DU TYPE
			offset +=4;
			var type:String = inStream.readMultiByte(4, "utf-8");
			log(offset+"-11 : Type : "+type);
			if(type!="WAVE") {
				throw new Error("UnsuportedType : "+type);
			}
			offset +=4;
			var chunktype:String;
			var chunklen:int;
			// LECTURE DES CHUNKS
			while(inStream.bytesAvailable) {
				chunktype = inStream.readMultiByte(4, "utf-8");
				chunklen = inStream.readInt();
				log(offset+"-"+(offset+7)+" : "+chunktype+"("+chunklen+")");  
				offset +=8;
				var chunk:ByteArray = new ByteArray();
				chunk.endian = Endian.LITTLE_ENDIAN;
				if(chunktype=="fmt ") {
					inStream.readBytes(chunk, 0, chunklen);
					fmtChunk= new FmtChunk(chunk);
					sampleRate = fmtChunk.sampleRate;
					factorToFloat = Math.pow(2, fmtChunk.bitsPerSample)/2
					log("SampleRate : "+sampleRate);
					log("BitsPerSample : "+fmtChunk.bitsPerSample);
					if(fmtChunk.bitsPerSample==8) { // 8 bits
						readMethodName = "readByte";
						nbBytesBySample = 1; // 1 octets par voie et par echantillon
					} else if(fmtChunk.bitsPerSample==16) { // 16 bits
						readMethodName = "readShort";
						nbBytesBySample = 2; // 2 octets par voie et par echantillon
					} else if(fmtChunk.bitsPerSample==32) { // 32 bits
						readMethodName = "readInt";
						nbBytesBySample = 4; // 4 octets par voie et par echantillon 
					}
				} else if(chunktype=="LIST") {
					inStream.readBytes(chunk, 0, chunklen);
					listChunk = new ListChunk(chunk);
				} else if(chunktype=="data") {
					startOffsetDatas = offset;
					_bytesTotal = chunklen;
					break;
				} 
				offset +=chunklen;
			}
			log("SoundDuration : "+this.length+" ms");
		}

		/**
		 * Nombre de bytes chargé de la partie sound sans les entetes
		 * en fait la taille total
		 */
		override public function get bytesLoaded():uint	{
			return this._bytesTotal;
		}
		
		/**
		 * Nombre de bytes total de la partie sound
		 */
		override public function get bytesTotal():int {
			return this._bytesTotal;
		}
		
		/**
		 * Ferme le stream
		 */
		override public function close():void {
			inStream.close();
		}
		
		/**
		 * Retourne l'objet ID3 comme pour un mp3 mais renseigné à partir des infos du wav
		 */
		override public function get id3():ID3Info {
			if(!this._id3) {
				this._id3 = new ID3Info();
				this._id3.album = this.listChunk.infoChunk.tags["IPRD"] || ""; 
				this._id3.artist = this.listChunk.infoChunk.tags["IART"] || ""; 
				this._id3.comment = this.listChunk.infoChunk.tags["ICMT"] || ""; 
				this._id3.genre = this.listChunk.infoChunk.tags["IGNR"] || ""; 
				this._id3.songName = this.listChunk.infoChunk.tags["INAM"] || ""; 
				this._id3.track = ""; 
				this._id3.year = this.listChunk.infoChunk.tags["ICRD"] || ""; 
			}
			return this._id3;
		}
		
		/**
		 * Return false
		 */
		override public function get isBuffering():Boolean {
			return false;
		}
		
		/**
		 * durée du son en millisecondes
		 */
		override public function get length():Number {
			if(!_len) {
				var avgBytesPerSec:int = (fmtChunk.bitsPerSample/8) * fmtChunk.sampleRate * fmtChunk.numChannels;
				_len = (_bytesTotal / avgBytesPerSec)*1000 ; 
			}
			return _len;
		}
		
		/**
		 * Chargement du son wav
		 * URLRequest doit pointer sur un fichier local 
		 */
		override public function load(stream:URLRequest, context:SoundLoaderContext=null):void {
			this._url = null;
			if(stream) {
				t0 = new Date().time;
				this._url = stream.url;
				analyseWav(stream);
				dispatchEvent(new Event(Event.COMPLETE));
				log("temps dechargement du wav: "+(new Date().time-t0)+"ms");
			}
		}
		
		/**
		 * joue le son
		 */
		override public function play(startTime:Number=0, loops:int=0, sndTransform:SoundTransform=null):SoundChannel {
			var startTimeSec:int = startTime/1000;
			playposition = startOffsetDatas + (startTimeSec * nbBytesBySample * fmtChunk.sampleRate * fmtChunk.numChannels);
			return super.play(startTime, loops, sndTransform);
		}
		
		/**
		 * Retourne l'url du fichier wav
		 */
		override public function get url():String {
			return this._url;
		}
		
		/**
		 * Transform le sample PCM en Float
		 */
		private function transformToFloat(sample:int):Number {
			return sample/factorToFloat;	
		}

		/**
		 * logging
		 */ 
		private function log(msg:String):void {
			if(debug) {
				trace(msg);			
			}
		}
	}
}